Skip to content

Commit f0b4adc

Browse files
authored
fix(gms): add rest.li validation in gms (datahub-project#2745)
1 parent 8fc1947 commit f0b4adc

File tree

4 files changed

+87
-2
lines changed

4 files changed

+87
-2
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.linkedin.metadata.resources;
2+
3+
import com.linkedin.common.urn.UrnValidator;
4+
import com.linkedin.data.schema.validation.ValidateDataAgainstSchema;
5+
import com.linkedin.data.schema.validation.ValidationOptions;
6+
import com.linkedin.data.schema.validation.ValidationResult;
7+
import com.linkedin.data.template.RecordTemplate;
8+
import com.linkedin.restli.common.HttpStatus;
9+
import com.linkedin.restli.server.RestLiServiceException;
10+
11+
12+
public class ResourceUtils {
13+
14+
private static final ValidationOptions DEFAULT_VALIDATION_OPTIONS = new ValidationOptions();
15+
private static final UrnValidator URN_VALIDATOR = new UrnValidator();
16+
17+
/**
18+
* Validates a {@link RecordTemplate} and throws {@link com.linkedin.restli.server.RestLiServiceException}
19+
* if validation fails.
20+
*
21+
* @param record record to be validated.
22+
* @param status the status code to return to the client on failure.
23+
*/
24+
public static void validateRecord(RecordTemplate record, HttpStatus status) {
25+
final ValidationResult result = ValidateDataAgainstSchema.validate(
26+
record,
27+
DEFAULT_VALIDATION_OPTIONS,
28+
URN_VALIDATOR);
29+
if (!result.isValid()) {
30+
throw new RestLiServiceException(status, result.getMessages().toString());
31+
}
32+
}
33+
34+
private ResourceUtils() { }
35+
36+
}

gms/impl/src/main/java/com/linkedin/metadata/resources/entity/AspectResource.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.linkedin.metadata.entity.EntityService;
66
import com.linkedin.metadata.restli.RestliUtils;
77
import com.linkedin.parseq.Task;
8+
import com.linkedin.restli.common.HttpStatus;
89
import com.linkedin.restli.server.annotations.Optional;
910
import com.linkedin.restli.server.annotations.QueryParam;
1011
import com.linkedin.restli.server.annotations.RestLiCollection;
@@ -18,6 +19,9 @@
1819
import org.slf4j.Logger;
1920
import org.slf4j.LoggerFactory;
2021

22+
import static com.linkedin.metadata.resources.ResourceUtils.*;
23+
24+
2125
/**
2226
* Single unified resource for fetching, updating, searching, & browsing DataHub entities
2327
*/
@@ -46,9 +50,10 @@ public Task<VersionedAspect> get(
4650
final VersionedAspect aspect = _entityService.getVersionedAspect(urn, aspectName, version);
4751
if (aspect == null) {
4852
throw RestliUtils.resourceNotFoundException();
53+
} else {
54+
validateRecord(aspect, HttpStatus.S_500_INTERNAL_SERVER_ERROR);
4955
}
5056
return aspect;
5157
});
5258
}
53-
5459
}

gms/impl/src/main/java/com/linkedin/metadata/resources/entity/EntityResource.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
import com.linkedin.metadata.search.SearchService;
1717
import com.linkedin.metadata.search.utils.BrowsePathUtils;
1818
import com.linkedin.parseq.Task;
19+
import com.linkedin.restli.common.HttpStatus;
20+
import com.linkedin.restli.common.validation.RestLiDataValidator;
1921
import com.linkedin.restli.server.annotations.Action;
2022
import com.linkedin.restli.server.annotations.ActionParam;
2123
import com.linkedin.restli.server.annotations.Optional;
2224
import com.linkedin.restli.server.annotations.QueryParam;
2325
import com.linkedin.restli.server.annotations.RestLiCollection;
2426
import com.linkedin.restli.server.annotations.RestMethod;
27+
import com.linkedin.restli.server.annotations.ValidatorParam;
2528
import com.linkedin.restli.server.resources.CollectionResourceTaskTemplate;
2629
import java.net.URISyntaxException;
2730
import java.time.Clock;
@@ -39,6 +42,7 @@
3942
import org.slf4j.LoggerFactory;
4043

4144
import static com.linkedin.metadata.PegasusUtils.urnToEntityName;
45+
import static com.linkedin.metadata.resources.ResourceUtils.*;
4246
import static com.linkedin.metadata.restli.RestliConstants.ACTION_AUTOCOMPLETE;
4347
import static com.linkedin.metadata.restli.RestliConstants.ACTION_BROWSE;
4448
import static com.linkedin.metadata.restli.RestliConstants.ACTION_GET_BROWSE_PATHS;
@@ -95,6 +99,8 @@ public Task<Entity> get(@Nonnull String urnStr, @QueryParam(PARAM_ASPECTS) @Opti
9599
final Entity entity = _entityService.getEntity(urn, projectedAspects);
96100
if (entity == null) {
97101
throw RestliUtils.resourceNotFoundException();
102+
} else {
103+
validateRecord(entity, HttpStatus.S_500_INTERNAL_SERVER_ERROR);
98104
}
99105
return entity;
100106
});
@@ -116,13 +122,17 @@ public Task<Map<String, Entity>> batchGet(
116122
return _entityService.getEntities(urns, projectedAspects)
117123
.entrySet()
118124
.stream()
125+
.peek(entry -> validateRecord(entry.getValue(), HttpStatus.S_500_INTERNAL_SERVER_ERROR))
119126
.collect(Collectors.toMap(entry -> entry.getKey().toString(), Map.Entry::getValue));
120127
});
121128
}
122129

123130
@Action(name = ACTION_INGEST)
124131
@Nonnull
125-
public Task<Void> ingest(@ActionParam(PARAM_ENTITY) @Nonnull Entity entity) throws URISyntaxException {
132+
public Task<Void> ingest(@ActionParam(PARAM_ENTITY) @Nonnull Entity entity, @ValidatorParam RestLiDataValidator validator) throws URISyntaxException {
133+
134+
validateRecord(entity, HttpStatus.S_422_UNPROCESSABLE_ENTITY);
135+
126136
final Set<String> projectedAspects = new HashSet<>(Arrays.asList("browsePaths"));
127137
final RecordTemplate snapshotRecord = RecordUtils.getSelectedRecordTemplateFromUnion(entity.getValue());
128138
final Urn urn = com.linkedin.metadata.dao.utils.ModelUtils.getUrnFromSnapshot(snapshotRecord);
@@ -144,6 +154,10 @@ public Task<Void> ingest(@ActionParam(PARAM_ENTITY) @Nonnull Entity entity) thro
144154
@Action(name = ACTION_BATCH_INGEST)
145155
@Nonnull
146156
public Task<Void> batchIngest(@ActionParam(PARAM_ENTITIES) @Nonnull Entity[] entities) throws URISyntaxException {
157+
for (Entity entity : entities) {
158+
validateRecord(entity, HttpStatus.S_422_UNPROCESSABLE_ENTITY);
159+
}
160+
147161
final AuditStamp auditStamp =
148162
new AuditStamp().setTime(_clock.millis()).setActor(Urn.createFromString(DEFAULT_ACTOR));
149163
return RestliUtils.toTask(() -> {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.linkedin.common.urn;
2+
3+
import com.linkedin.data.message.Message;
4+
import com.linkedin.data.schema.DataSchema;
5+
import com.linkedin.data.schema.NamedDataSchema;
6+
import com.linkedin.data.schema.validator.Validator;
7+
import com.linkedin.data.schema.validator.ValidatorContext;
8+
import java.net.URISyntaxException;
9+
10+
11+
/**
12+
* Rest.li Validator responsible for ensuring that {@link Urn} objects are well-formed.
13+
*
14+
* Note that this validator does not validate the integrity of strongly typed urns,
15+
* or validate Urn objects against their associated key aspect.
16+
*/
17+
public class UrnValidator implements Validator {
18+
@Override
19+
public void validate(ValidatorContext context) {
20+
if (DataSchema.Type.TYPEREF.equals(context.dataElement().getSchema().getType())
21+
&& ((NamedDataSchema) context.dataElement().getSchema()).getName().endsWith("Urn")) {
22+
try {
23+
Urn.createFromString((String) context.dataElement().getValue());
24+
} catch (URISyntaxException e) {
25+
context.addResult(new Message(context.dataElement().path(), "\"Provided urn %s\" is invalid", context.dataElement().getValue()));
26+
context.setHasFix(false);
27+
}
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)