Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
899be42
init
christiangoerdes May 28, 2026
9a15d18
Implement JSON-RPC request validation with rules and batch support
christiangoerdes May 28, 2026
69d37c7
Add unit tests for JsonRPCProtectionInterceptor
christiangoerdes May 28, 2026
9b7da29
Refactor JSON-RPC validation logic into JsonRPCValidator for improved…
christiangoerdes May 28, 2026
9222fc2
Add JSON-RPC parameter schema validation with unit tests
christiangoerdes May 28, 2026
89e6590
Support JSON-RPC parameter schema validation with regex-based method …
christiangoerdes May 28, 2026
8a57b48
Merge branch 'master' into json-rpc-protection
christiangoerdes May 28, 2026
5a225af
Make `fromNode` method public in JSONRPCRequest, add logging to JsonR…
christiangoerdes May 28, 2026
329ee81
Merge remote-tracking branch 'origin/json-rpc-protection' into json-r…
christiangoerdes May 28, 2026
5bd32da
Add Javadoc comments for JSON-RPC protection classes with detailed de…
christiangoerdes May 28, 2026
4fd835a
Support XML-style parameter mappings for JSON-RPC schema validation a…
christiangoerdes May 28, 2026
de22caa
Refactor JSON-RPC validation: rename `rules` to `methods` and improve…
predic8 May 28, 2026
c4b7d4d
Merge remote-tracking branch 'origin/json-rpc-protection' into json-r…
predic8 May 28, 2026
6825c32
Fix test failures: set batch rules before interceptor init
christiangoerdes Jun 1, 2026
7d431c3
Filter child elements excluded from JSON schema in `JsonSchemaGenerat…
christiangoerdes Jun 1, 2026
c25259c
Refactor Allow/Deny rule hierarchy: move to util.allowdeny, rename me…
christiangoerdes Jun 1, 2026
fd8d1f7
Reject non-JSON content types in `JsonRPCProtectionInterceptor` with …
christiangoerdes Jun 1, 2026
7ce7007
Merge branch 'master' into json-rpc-protection
christiangoerdes Jun 1, 2026
c1bab2b
Move Allow/Deny rules to `util.config.allowdeny` package and update a…
christiangoerdes Jun 1, 2026
f52a9a6
Add JSON-RPC protection tutorial with method filtering, parameter val…
christiangoerdes Jun 1, 2026
cd0bd99
Add tests for JSON-RPC protection tutorial covering method filtering,…
christiangoerdes Jun 1, 2026
fe0833b
Switch JSON-RPC `params` method matching from regex to exact names an…
christiangoerdes Jun 1, 2026
f7d963f
Add result schema validation to JSON-RPC protection and refactor sche…
christiangoerdes Jun 1, 2026
f223210
Add JSON-RPC result schema validation for `rpc.echo` with test update…
christiangoerdes Jun 1, 2026
92bb547
disable response handling when result mappings are empty
christiangoerdes Jun 1, 2026
0b4ed0c
Update JSON-RPC protection tutorial and tests to use consistent `id` …
christiangoerdes Jun 1, 2026
2519939
Add schema validation support for JSON-RPC `params`, `response`, and …
christiangoerdes Jun 5, 2026
79f985a
Remove deprecated JSON-RPC `params` and `result` schema handling, rep…
christiangoerdes Jun 5, 2026
f957fdf
Remove redundant body empty check in `JsonRPCProtectionInterceptor` s…
christiangoerdes Jun 5, 2026
24db336
Add copyright headers to JSON-RPC interceptor classes
christiangoerdes Jun 5, 2026
666c123
Merge branch 'master' into json-rpc-protection
christiangoerdes Jun 5, 2026
7ba583f
Add inline schema support for JSON-RPC `error` validation, enhance do…
christiangoerdes Jun 5, 2026
63ec1ea
Merge remote-tracking branch 'origin/json-rpc-protection' into json-r…
christiangoerdes Jun 5, 2026
f7d5cbd
Use inline schema in tutorial
christiangoerdes Jun 5, 2026
7a1e798
Add response schema validation for JSON-RPC methods, update tests wit…
christiangoerdes Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import static com.predic8.membrane.annot.Constants.JSON_SCHEMA_VERSION;
import static com.predic8.membrane.annot.generator.kubernetes.model.SchemaFactory.*;
import static com.predic8.membrane.annot.generator.util.SchemaGeneratorUtil.escapeJsonContent;
import static com.predic8.membrane.annot.model.OtherAttributesInfo.ValueType.STRING;
import static javax.tools.StandardLocation.CLASS_OUTPUT;

/**
Expand Down Expand Up @@ -100,7 +101,7 @@ private void addTopLevelProperties(Model m, MainInfo main) {
private void addParserDefinitions(Model m, MainInfo main) {
for (ElementInfo elementInfo : main.getElements().values()) {

if (elementInfo.getAnnotation().mixed() && !elementInfo.getChildElementSpecs().isEmpty()) {
if (elementInfo.getAnnotation().mixed() && !getJsonVisibleChildElementSpecs(elementInfo).isEmpty()) {
throw new ProcessingException(
"@MCElement(..., mixed=true) and @MCTextContent is not compatible with @MCChildElement.",
elementInfo.getElement()
Expand Down Expand Up @@ -140,7 +141,14 @@ private static AbstractSchema<?> tagElementId(AbstractSchema<?> schema, ElementI

private AbstractSchema<?> createNoEnvelopeParser(Model model, MainInfo main, ElementInfo elementInfo, String parserName) {
// With noEnvelope=true, there should be exactly one child element
ChildElementInfo childSpec = elementInfo.getChildElementSpecs().getFirst();
List<ChildElementInfo> visibleChildElementSpecs = getJsonVisibleChildElementSpecs(elementInfo);
if (visibleChildElementSpecs.isEmpty()) {
throw new ProcessingException(
"@MCElement(noEnvelope=true) must declare at least one JSON-visible @MCChildElement.",
elementInfo.getElement()
);
}
ChildElementInfo childSpec = visibleChildElementSpecs.getFirst();
String childName = childSpec.getPropertyName();

boolean flowParserType = shouldGenerateFlowParserType(childSpec);
Expand Down Expand Up @@ -198,7 +206,7 @@ private AbstractSchema<?> createCollapsedInlineParser(ElementInfo ei, String par
var attrs = ei.getAis().stream().toList();

boolean hasText = ei.getTci() != null;
boolean hasChildren = !ei.getChildElementSpecs().isEmpty();
boolean hasChildren = !getJsonVisibleChildElementSpecs(ei).isEmpty();

if (hasChildren) {
throw new ProcessingException("@MCElement(collapsed=true) must not declare child elements.", ei.getElement());
Expand Down Expand Up @@ -240,7 +248,7 @@ private AbstractSchema<?> createCollapsedInlineParser(ElementInfo ei, String par

private SchemaObject getParserSchemaObject(ElementInfo elementInfo, String parserName) {
return object(parserName)
.additionalProperties(elementInfo.isString())
.additionalProperties(false)
.description(getDescriptionContent(elementInfo));
}

Expand Down Expand Up @@ -305,6 +313,29 @@ private void collectProperties(Model model, MainInfo main, ElementInfo elementIn
processMCAttributes(elementInfo, parserSchema);
collectTextContent(elementInfo, parserSchema);
processMCChilds(model, main, elementInfo, parserSchema);
processMCOtherAttributes(model, main, elementInfo, parserSchema);
}

private void processMCOtherAttributes(Model model, MainInfo main, ElementInfo elementInfo, SchemaObject parserSchema) {
var otherAttributes = elementInfo.getOai();
if (otherAttributes == null) {
return;
}

if (otherAttributes.getValueType() == STRING) {
parserSchema.additionalProperties(from("string"));
return;
}

var valueElementInfo = main.getElements().get(otherAttributes.getMapValueType());
if (valueElementInfo == null) {
parserSchema.additionalProperties(true);
return;
}

parserSchema.additionalProperties(
ref("additionalProperties").ref(defsRefPath(valueElementInfo.getXSDTypeName(model)))
);
}

private void collectTextContent(ElementInfo elementInfo, SchemaObject parserSchema) {
Expand All @@ -318,7 +349,7 @@ private void collectTextContent(ElementInfo elementInfo, SchemaObject parserSche
}

private void processMCChilds(Model model, MainInfo main, ElementInfo parentElementInfo, AbstractSchema<?> parentSchema) {
for (ChildElementInfo childSpec : parentElementInfo.getChildElementSpecs()) {
for (ChildElementInfo childSpec : getJsonVisibleChildElementSpecs(parentElementInfo)) {

if (!childSpec.isList()) {
if (parentSchema instanceof SchemaObject parentObjectSchema) {
Expand Down Expand Up @@ -537,7 +568,7 @@ private SchemaObject createComponentsMapParser(Model m, MainInfo main, ElementIn
}

private boolean hasComponentChild(ElementInfo parentElementInfo, MainInfo main) {
for (ChildElementInfo childSpec : parentElementInfo.getChildElementSpecs()) {
for (ChildElementInfo childSpec : getJsonVisibleChildElementSpecs(parentElementInfo)) {
var childDeclaration = getChildElementDeclarationInfo(main, childSpec);
if (childDeclaration == null) continue;

Expand Down Expand Up @@ -586,11 +617,17 @@ private boolean hasAnyConfigurableProperty(ElementInfo elementInfo, MainInfo mai
.filter(attributeInfo -> !attributeInfo.excludedFromJsonSchema())
.anyMatch(attributeInfo -> !"id".equals(attributeInfo.getXMLName()))
|| elementInfo.getTci() != null
|| !elementInfo.getChildElementSpecs().isEmpty()
|| !getJsonVisibleChildElementSpecs(elementInfo).isEmpty()
|| elementInfo.getOai() != null
|| hasComponentChild(elementInfo, main);
}

private static List<ChildElementInfo> getJsonVisibleChildElementSpecs(ElementInfo elementInfo) {
return elementInfo.getChildElementSpecs().stream()
.filter(childElementInfo -> !childElementInfo.excludedFromJsonSchema())
.toList();
}

private void setItemsIfArray(AbstractSchema<?> parentSchema, AbstractSchema<?> itemsSchema) {
if (parentSchema instanceof SchemaArray schemaArray) {
schemaArray.items(itemsSchema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import java.util.*;

import static com.predic8.membrane.annot.generator.kubernetes.model.SchemaFactory.*;
import static java.lang.Boolean.FALSE;

public class SchemaObject extends AbstractSchema<SchemaObject> {

private boolean additionalProperties;
private Boolean additionalProperties;
private AbstractSchema<?> additionalPropertiesSchema;

// Java Properties (@MCAttributes, @MCChildElement)
protected final List<AbstractSchema<?>> properties = new ArrayList<>();
Expand Down Expand Up @@ -54,7 +56,9 @@ public ObjectNode json(ObjectNode node) {
if (minProperties != null) node.put("minProperties", minProperties);
if (maxProperties != null) node.put("maxProperties", maxProperties);

if (!additionalProperties && isObject()) {
if (additionalPropertiesSchema != null && isObject()) {
node.set("additionalProperties", additionalPropertiesSchema.json(jnf.objectNode()));
} else if (FALSE.equals(additionalProperties) && isObject()) {
node.put("additionalProperties", false);
}

Expand All @@ -75,6 +79,13 @@ public SchemaObject property(AbstractSchema<?> as) {

public SchemaObject additionalProperties(boolean additionalProperties) {
this.additionalProperties = additionalProperties;
this.additionalPropertiesSchema = null;
return this;
}

public SchemaObject additionalProperties(AbstractSchema<?> additionalPropertiesSchema) {
this.additionalProperties = null;
this.additionalPropertiesSchema = additionalPropertiesSchema;
return this;
}

Expand Down Expand Up @@ -192,4 +203,4 @@ public SchemaObject allOf(List<AbstractSchema<?>> allOf) {
public boolean hasProperty(String name) {
return properties.stream().anyMatch(p -> name.equals(p.getName()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ public void setList(boolean list) {
this.list = list;
}

public boolean excludedFromJsonSchema() {
return annotation != null && annotation.excludeFromJson();
}

@Override
public String toString() {
return "ChildElementInfo{" +
Expand All @@ -105,4 +109,4 @@ public String toString() {
", required=" + required +
'}';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ public ValueType getValueType() {
if (mapValueType.getQualifiedName().toString().equals("java.lang.String")) {
return ValueType.STRING;
}
if (mapValueType.getQualifiedName().toString().equals("java.lang.Object")) {
return ValueType.OBJECT;
}
throw new ProcessingException("Not supported: @McOtherAttributes void setAttr(Map<String, T> attrs) where T is neither String nor Object.");
return ValueType.OBJECT;
}

public TypeElement getMapValueType() {
return mapValueType;
}

public enum ValueType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static boolean isSetter(Method method) {
}

public static boolean isStructured(Method method) {
return findAnnotation(method, MCChildElement.class) != null;
return isJsonVisibleChild(method);
}

public static boolean matchesJsonKey(Method method, String key) {
Expand All @@ -48,7 +48,7 @@ public static boolean matchesJsonKey(Method method, String key) {
}

private static boolean matchesJsonChildElementKey(Method method, String key) {
return findAnnotation(method, MCChildElement.class) != null
return isJsonVisibleChild(method)
&& matchesPropertyName(method, key);
}

Expand Down Expand Up @@ -101,7 +101,7 @@ public static <T> Method getSingleChildSetter(ParsingContext pc, Class<T> clazz)
private static <T> @NotNull List<Method> getChildSetters(Class<T> clazz) {
List<Method> childSetters = stream(clazz.getMethods())
.filter(McYamlIntrospector::isSetter)
.filter(method -> findAnnotation(method, MCChildElement.class) != null)
.filter(McYamlIntrospector::isJsonVisibleChild)
.toList();
if (childSetters.isEmpty()) {
throw new RuntimeException("No @MCChildElement setter found in " + clazz.getName());
Expand Down Expand Up @@ -155,7 +155,7 @@ public static boolean hasAttributes(Class<?> clazz) {
}

public static boolean hasChildren(Class<?> clazz) {
return stream(clazz.getMethods()).anyMatch(m -> m.isAnnotationPresent(MCChildElement.class));
return stream(clazz.getMethods()).anyMatch(McYamlIntrospector::isJsonVisibleChild);
}

public static <T> Method getAnySetter(Class<T> clazz) {
Expand All @@ -169,7 +169,7 @@ public static <T> Method getAnySetter(Class<T> clazz) {
public static <T> Method getChildSetter(Class<T> clazz, Class<?> valueClass) {
return stream(clazz.getMethods())
.filter(McYamlIntrospector::isSetter)
.filter(McYamlIntrospector::isStructured)
.filter(McYamlIntrospector::isJsonVisibleChild)
.filter(method -> method.getParameterTypes().length == 1)
.filter(method -> method.getParameterTypes()[0].isAssignableFrom(valueClass))
.reduce((a, b) -> {
Expand All @@ -178,6 +178,11 @@ public static <T> Method getChildSetter(Class<T> clazz, Class<?> valueClass) {
.orElseThrow(() -> new RuntimeException("Could not find child setter on %s for value of type %s".formatted(clazz.getName(), valueClass.getName())));
}

private static boolean isJsonVisibleChild(Method method) {
MCChildElement annotation = findAnnotation(method, MCChildElement.class);
return annotation != null && !annotation.excludeFromJson();
}

public static boolean isReferenceAttribute(Method setter) {
if (findAnnotation(setter, MCAttribute.class) == null)
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public ParsingContext<?> addPath(String path) {
return new ParsingContext(context, registry,grammar,topLevel, this.path + path,key);
}

public ParsingContext<?> addProperty(String property) {
return new ParsingContext(context, registry, grammar, topLevel, path + toJsonPathProperty(property), key);
}

public ParsingContext<?> child(String childContext, String pathSegment) {
return new ParsingContext(childContext, registry, grammar, topLevel, path + pathSegment, null);
}
Expand Down Expand Up @@ -84,6 +88,16 @@ public String getPath() {
return path;
}

private static String toJsonPathProperty(String property) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs

if (property.matches("[A-Za-z_][A-Za-z0-9_]*")) {
return "." + property;
}
return "['" + property
.replace("\\", "\\\\")
.replace("'", "\\'")
+ "']";
}

@Override
public @NotNull String toString() {
return """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,31 +283,40 @@ private static int getIndentation(String line) {
}

private static String getParentPath(String jsonPath) {
// Handle both $.parent.child and $.parent[0] formats
int lastDot = jsonPath.lastIndexOf('.');
int lastBracket = jsonPath.lastIndexOf('[');
return jsonPath.substring(0, findLastSegmentStart(jsonPath));
}

if (lastBracket > lastDot) {
// Last segment is array index like [0]
return jsonPath.substring(0, lastBracket);
} else {
// Last segment is object key like .field
return jsonPath.substring(0, lastDot);
private static String getLastSegment(String jsonPath) {
int start = findLastSegmentStart(jsonPath);
String segment = jsonPath.substring(start);

if (segment.startsWith(".")) {
return segment.substring(1);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc sample ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract

if (segment.startsWith("['") && segment.endsWith("']")) {
return segment.substring(2, segment.length() - 2)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract

.replace("\\'", "'")
.replace("\\\\", "\\");
}

if (segment.startsWith("[") && segment.endsWith("]")) {
return segment.substring(1, segment.length() - 1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract

}

throw new IllegalArgumentException("Unsupported JSONPath segment: " + segment);
}

private static String getLastSegment(String jsonPath) {
// Handle both $.parent.child and $.parent[0] formats
int lastDot = jsonPath.lastIndexOf('.');
private static int findLastSegmentStart(String jsonPath) {
int lastBracket = jsonPath.lastIndexOf('[');
int lastDot = jsonPath.lastIndexOf('.');

if (lastBracket > lastDot) {
// Array index like [0]
String bracket = jsonPath.substring(lastBracket);
return bracket.substring(1, bracket.length() - 1); // Extract "0" from "[0]"
} else {
// Object key like .field
return jsonPath.substring(lastDot + 1);
return lastBracket;
}
if (lastDot >= 0) {
return lastDot;
}
throw new IllegalArgumentException("Cannot determine parent path of: " + jsonPath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ private Object resolveSetterValue(ParsingContext<?> ctx, JsonNode node, String k
// Structured objects
if (McYamlIntrospector.isStructured(setter)) {
if (beanClass != null)
return ObjectBinder.bind(ctx.updateContext(key).addPath("." + key), beanClass, node);
return ObjectBinder.bind(ctx.updateContext(key).addPath("." + key), wanted, node);
return ObjectBinder.bind(ctx.updateContext(key).addProperty(key), beanClass, node);
return ObjectBinder.bind(ctx.updateContext(key).addProperty(key), wanted, node);
}

return coerceScalarOrReference(ctx, node, key, wanted);
Expand All @@ -103,7 +103,7 @@ Object coerceScalarOrReference(ParsingContext<?> ctx, JsonNode node, String key,
return null;

Class<?> elemType = getCollectionElementType(setter);
List<Object> list = CollectionBinder.parseListIncludingStartEvent(ctx.addPath("." + key), node, elemType);
List<Object> list = CollectionBinder.parseListIncludingStartEvent(ctx.addProperty(key), node, elemType);
if (elemType != null) {
for (Object o : list) {
if (o == null) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static Object parseMapToObj(ParsingContext<?> pc, JsonNode node) {
private static Object parseMapToObj(ParsingContext<?> ctx, JsonNode node, String key) {
if ("$ref".equals(key))
return REFERENCE_RESOLVER.resolveReferencedObject(ctx, node.asText(), key);
var childContext = ctx.addPath("." + key);
var childContext = ctx.addProperty(key);
return ObjectBinder.bind(childContext.updateContext(key), childContext.resolveClass(key), node);
}

Expand Down
Loading
Loading