Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b99f83a
fix: validate custom property name charset
sonika-shah Apr 29, 2026
d4c2013
Update generated TypeScript types
github-actions[bot] Apr 29, 2026
9b7eb18
test: add schema-vs-Java consistency test for custom property name
sonika-shah Apr 29, 2026
ec6582f
fix: extend custom property name charset after gap-coverage matrix
sonika-shah Apr 29, 2026
452bc25
Update generated TypeScript types
github-actions[bot] Apr 29, 2026
9aba5b3
updated the custom property name validation
Rohit0301 Apr 29, 2026
021647a
added name suffix in custom property name
Rohit0301 Apr 29, 2026
a68bd03
lint fixes
Rohit0301 Apr 29, 2026
4f033ea
include backslash in invalid char
Rohit0301 Apr 30, 2026
be23e01
fixed the playwright issue
Rohit0301 Apr 30, 2026
1823307
lint fix
Rohit0301 Apr 30, 2026
28ec255
Merge branch 'main' into fix/custom-property-name-charset-validation
Rohit0301 May 2, 2026
d490b3d
Merge branch 'main' into fix/custom-property-name-charset-validation
sonika-shah May 4, 2026
fa82046
fix check style
sonika-shah May 4, 2026
3c92e1b
Drop redundant Java validator for custom property name; tighten IT as…
sonika-shah May 4, 2026
ef4d807
restrict few more special characters from Cp name
Rohit0301 May 5, 2026
0129bf1
minor fix
Rohit0301 May 5, 2026
34f3163
Disallow & < > in custom property names; align IT cases
sonika-shah May 5, 2026
5c3fbe2
Update generated TypeScript types
github-actions[bot] May 5, 2026
0dd1684
Close PATCH bypass for custom property name validation on Type
sonika-shah May 5, 2026
722f2f2
Block * in custom property names — breaks ES field-path lookup
sonika-shah May 5, 2026
1864b0a
Validate only newly-added custom properties; isolate PATCH IT to fres…
sonika-shah May 5, 2026
40738ed
chore: prettier formatting on the new asterisk-rejection test
sonika-shah May 5, 2026
04eef82
Potential fix for pull request finding
Rohit0301 May 5, 2026
dd494e9
docs: add + ? ~ ` to JSDoc allow-list to match the regex
sonika-shah May 5, 2026
d46af48
fix(it): request customProperties field on read-back in PATCH IT
sonika-shah May 5, 2026
885d958
Merge branch 'main' into fix/custom-property-name-charset-validation
sonika-shah May 6, 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 @@ -324,6 +324,164 @@ void test_typeWithInvalidName_fails(TestNamespace ns) {
}
}

@Test
void test_customPropertyNameAllowedCharacters_succeeds(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("safe");
Comment on lines +329 to +333

String[] allowedNames = {
prefix + "Plain",
prefix + "_underscore",
prefix + "-hyphen",
prefix + ".dot",
prefix + "with space",
prefix + "with/slash",
prefix + "with&amp",
prefix + "with%percent",
prefix + "with#hash",
prefix + "with@at",
prefix + "with!bang",
prefix + "with,comma",
prefix + "with;semi",
prefix + "with=eq",
prefix + "with|pipe",
prefix + "with'quote",
prefix + "with(lparen",
prefix + "with)rparen",
prefix + "with<lt",
prefix + "with>gt",
prefix + "with[lbrack",
prefix + "with]rbrack",
prefix + "with{lbrace",
prefix + "with}rbrace",
prefix + "with+plus",
prefix + "with?question",
prefix + "with*asterisk",
prefix + "with~tilde",
prefix + "with`backtick",
prefix + "withMatched(pair)And<more>",
prefix + "withDigits123",
};

for (String name : allowedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Allowed-charset test for custom property name");
property.setPropertyType(STRING_TYPE.getEntityReference());

Type updatedType = addCustomProperty(client, tableTypeId, property);
assertNotNull(updatedType, "Allowed name '" + name + "' must be accepted");

boolean present =
updatedType.getCustomProperties().stream().anyMatch(cp -> name.equals(cp.getName()));
assertTrue(present, "Custom property '" + name + "' should be saved on the type");
}
}

@Test
void test_customPropertyNameDisallowedCharacters_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("bad");

String[] disallowedNames = {
prefix + "with\"dquote",
prefix + "with:colon",
prefix + "with^caret",
prefix + "with$dollar",
prefix + "with\\backslash",
};

for (String name : disallowedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Disallowed-charset test for custom property name");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
Exception.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name '" + name + "' should be rejected");
}
}

@Test
void test_customPropertyNameMustStartWithAlphanumeric_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("lead");

String[] invalidLeads = {
"_" + prefix, "-" + prefix, "." + prefix, " " + prefix, "(" + prefix, "<" + prefix,
};

for (String name : invalidLeads) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Leading-character validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
Exception.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name '" + name + "' must start with alphanumeric");
}
}

@Test
void test_customPropertyNameTooLong_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();

StringBuilder longName = new StringBuilder(ns.prefix("long"));
while (longName.length() <= 256) {
longName.append('a');
}

CustomProperty property = new CustomProperty();
property.setName(longName.toString());
property.setDescription("Length validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
Exception.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name longer than 256 characters should be rejected");
}

@Test
void test_customPropertyNameUnbalancedBrackets_succeeds(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("bracket");

String[] unbalancedNames = {
prefix + "openParen(",
prefix + "closeParen)",
prefix + "openLt<",
prefix + "closeGt>",
prefix + "openLbrack[",
prefix + "closeRbrack]",
prefix + "openLbrace{",
prefix + "closeRbrace}",
};

for (String name : unbalancedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Unbalanced-bracket validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

Type updatedType = addCustomProperty(client, tableTypeId, property);
assertNotNull(updatedType);

boolean present =
updatedType.getCustomProperties().stream().anyMatch(cp -> name.equals(cp.getName()));
assertTrue(present, "Unbalanced bracket name '" + name + "' should be saved");
}
}

@Test
void test_getEntityTypeFields() throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Triple;
Expand Down Expand Up @@ -67,6 +68,13 @@ public class TypeRepository extends EntityRepository<Type> {
private static final String PATCH_FIELDS = "customProperties";
private static final Striped<Lock> TYPE_PROPERTY_LOCKS = Striped.lock(4096);

// Must stay in sync with definitions.customPropertyName.pattern in
// openmetadata-spec/src/main/resources/json/schema/type/basic.json
// Enforced by TypeRepositoryTest.customPropertyNamePatternMatchesSchema.
static final Pattern CUSTOM_PROPERTY_NAME_PATTERN =
Pattern.compile("^[A-Za-z0-9][A-Za-z0-9 _\\-.,;/&%#@!'(){}\\[\\]<>|=+?*~`]*$");
static final int CUSTOM_PROPERTY_NAME_MAX_LENGTH = 256;

public TypeRepository() {
super(
TypeResource.COLLECTION_PATH,
Expand Down Expand Up @@ -213,6 +221,7 @@ private List<CustomProperty> getCustomProperties(Type type) {
}

private void validateProperty(CustomProperty customProperty) {
validateCustomPropertyName(customProperty.getName());
switch (customProperty.getPropertyType().getName()) {
case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig());
case "table-cp" -> validateTableTypeConfig(customProperty.getCustomPropertyConfig());
Expand All @@ -228,6 +237,27 @@ private void validateProperty(CustomProperty customProperty) {
}
}

private static void validateCustomPropertyName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Custom property name must not be empty");
}
if (name.length() > CUSTOM_PROPERTY_NAME_MAX_LENGTH) {
throw new IllegalArgumentException(
String.format(
"Custom property name '%s' exceeds maximum length of %d characters",
name, CUSTOM_PROPERTY_NAME_MAX_LENGTH));
}
if (!CUSTOM_PROPERTY_NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException(
String.format(
"Invalid custom property name '%s'. Name must start with an alphanumeric character "
+ "and may contain only: alphanumeric, _ - . / & %% # @ ! , ; = | ' + ? * ~ ` "
+ "space ( ) < > [ ] { }. "
+ "The following characters are not allowed: \" : ^ $ \\",
name));
}
}

private void validateDateFormat(
CustomPropertyConfig config, Set<Character> validTokens, String errorMessage) {
if (config != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.service.jdbi3;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.InputStream;
import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link TypeRepository}.
*
* <p>The schema-vs-Java consistency test guarantees that the regex enforced in Java stays in lock
* step with the JSON Schema definition referenced by {@code customProperty.json}. If either side
* is updated without the other, this test fails with a message pointing at both files.
*/
class TypeRepositoryTest {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String BASIC_SCHEMA_RESOURCE = "json/schema/type/basic.json";
private static final String CUSTOM_PROPERTY_NAME_DEF = "customPropertyName";

@Test
void customPropertyNamePatternMatchesSchema() throws Exception {
JsonNode schemaPattern = readDefinitionField(CUSTOM_PROPERTY_NAME_DEF, "pattern");
assertNotNull(
schemaPattern,
"customPropertyName.pattern must exist in " + BASIC_SCHEMA_RESOURCE);

assertEquals(
schemaPattern.asText(),
TypeRepository.CUSTOM_PROPERTY_NAME_PATTERN.pattern(),
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated
"JSON Schema pattern in basic.json#/definitions/"
+ CUSTOM_PROPERTY_NAME_DEF
+ " has drifted from TypeRepository.CUSTOM_PROPERTY_NAME_PATTERN. "
+ "Update both so the API and schema enforce the same rule.");
}

@Test
void customPropertyNameMaxLengthMatchesSchema() throws Exception {
JsonNode schemaMax = readDefinitionField(CUSTOM_PROPERTY_NAME_DEF, "maxLength");
assertNotNull(
schemaMax,
"customPropertyName.maxLength must exist in " + BASIC_SCHEMA_RESOURCE);

assertEquals(
schemaMax.asInt(),
TypeRepository.CUSTOM_PROPERTY_NAME_MAX_LENGTH,
"JSON Schema maxLength has drifted from TypeRepository.CUSTOM_PROPERTY_NAME_MAX_LENGTH. "
+ "Update both.");
}

@Test
void customPropertyNameDefinitionIsRequiredString() throws Exception {
JsonNode definition = readDefinition(CUSTOM_PROPERTY_NAME_DEF);
assertNotNull(definition, "customPropertyName must be defined in " + BASIC_SCHEMA_RESOURCE);
assertEquals("string", definition.get("type").asText());
assertTrue(
definition.get("minLength").asInt() >= 1,
"customPropertyName must have minLength >= 1");
}

private static JsonNode readDefinition(String definitionName) throws Exception {
try (InputStream in =
TypeRepositoryTest.class.getClassLoader().getResourceAsStream(BASIC_SCHEMA_RESOURCE)) {
assertNotNull(in, "Could not load resource " + BASIC_SCHEMA_RESOURCE);
JsonNode root = MAPPER.readTree(in);
return root.path("definitions").get(definitionName);
}
}

private static JsonNode readDefinitionField(String definitionName, String field)
throws Exception {
JsonNode definition = readDefinition(definitionName);
return definition == null ? null : definition.get(field);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@
"maxLength": 256,
"pattern": "^((?!::).)*$"
},
"customPropertyName": {
"description": "Name of a custom property. Allowed characters: alphanumeric, _ - . / & % # @ ! , ; = | ' + ? * ~ ` space ( ) < > [ ] { }. Must start with an alphanumeric character. Disallowed characters: \" : ^ $ \\",
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[A-Za-z0-9][A-Za-z0-9 _\\-.,;/&%#@!'(){}\\[\\]<>|=+?*~`]*$"
},
Comment thread
sonika-shah marked this conversation as resolved.
"testCaseEntityName": {
"description": "Name that identifies a test definition and test case.",
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
},
"properties": {
"name": {
"description": "Name of the entity property. Note a property name must be unique for an entity. Property name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
"$ref": "../type/basic.json#/definitions/entityName"
"description": "Name of the entity property. Must be unique for an entity. Allowed characters: alphanumeric, _ - . / & % # @ ! , ; = | ' + ? * ~ ` space ( ) < > [ ] { }. Must start with an alphanumeric character. Disallowed: \" : ^ $ \\.",
Comment thread
sonika-shah marked this conversation as resolved.
Outdated
"$ref": "../type/basic.json#/definitions/customPropertyName"
Comment thread
sonika-shah marked this conversation as resolved.
Outdated
},
"displayName": {
"description": "Display Name for the custom property.Must be unique for an entity.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,15 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
};

export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR =
"Name must not contain '::'.";
'Name must start with a letter or number. Invalid characters: " : ^ $ \\';

export const CUSTOM_PROPERTY_INVALID_NAMES = {
STARTS_WITH_SPECIAL_CHAR: '_invalidName',
DISALLOWED_COLON: 'name:with:colon',
DISALLOWED_DOLLAR: 'name$invalid',
DISALLOWED_CARET: 'name^invalid',
DISALLOWED_QUOTE: 'name"invalid',
DISALLOWED_BACKSLASH: String.raw`name\invalid`,
};

export const NAME_SUFFIX = ".!@#%`&*()_-=+{}[]~|;'<>,.?/";
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated
Loading
Loading