b/161829123 Create extension scope validator with validation for three extensions Change-Id: If11cfc7327f23f9f53e29415cceabcf11730a868
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionSchemaValidator.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionSchemaValidator.java index edfa743..2372979 100644 --- a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionSchemaValidator.java +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionSchemaValidator.java
@@ -1,5 +1,6 @@ package com.apigee.security.oas.extendedvalidator; +import static com.apigee.security.oas.extendedvalidator.ErrorMessage.INVALID_SCHEMA; import static com.apigee.security.oas.extendedvalidator.ExtensionName.valueOfExtensionName; import static com.apigee.security.oas.extendedvalidator.ExtensionValidators.defaultErrors; import static com.apigee.security.oas.extendedvalidator.ExtensionValidators.prepareNamedPath; @@ -73,7 +74,8 @@ JsonSchema schema = getJsonSchemaFromUrl(schemaUrl); Set<ValidationMessage> errors = schema.validate(extension.getExtensionContent()); - return toExtensionValidationMessages(errors, "INVALID_SCHEMA", prepareNamedPath(extension)); + return toExtensionValidationMessages( + errors, INVALID_SCHEMA.getErrorType(), prepareNamedPath(extension)); } catch (URISyntaxException e) { String errorMessage = "Failed to validate extension schema";
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidator.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidator.java new file mode 100644 index 0000000..8ff013a --- /dev/null +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidator.java
@@ -0,0 +1,94 @@ +package com.apigee.security.oas.extendedvalidator; + +import static com.apigee.security.oas.extendedvalidator.ErrorMessage.X_SECURITY_ALLOW_SCOPE_ERROR; +import static com.apigee.security.oas.extendedvalidator.ErrorMessage.X_SECURITY_TYPE_DEFINITIONS_SCOPE_ERROR; +import static com.apigee.security.oas.extendedvalidator.ErrorMessage.X_SECURITY_TYPE_SCOPE_ERROR; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_ALLOW; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_TYPE; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_TYPE_DEFINITIONS; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.valueOfExtensionName; +import static com.apigee.security.oas.extendedvalidator.ExtensionValidators.defaultErrors; +import static com.apigee.security.oas.extendedvalidator.ExtensionValidators.fetchExtensionParent; +import static com.apigee.security.oas.extendedvalidator.ExtensionValidators.prepareNamedPath; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; +import java.util.Optional; +import org.openapi4j.parser.model.OpenApiSchema; +import org.openapi4j.parser.model.v3.OpenApi3; +import org.openapi4j.parser.model.v3.Operation; +import org.openapi4j.parser.model.v3.Parameter; +import org.openapi4j.parser.model.v3.Schema; + +final class BaseExtensionScopeValidator implements ExtensionScopeValidator { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final ImmutableMap<ExtensionName, ErrorMessage> errorMessages = + Maps.immutableEnumMap( + ImmutableMap.of( + X_SECURITY_TYPE, X_SECURITY_TYPE_SCOPE_ERROR, + X_SECURITY_ALLOW, X_SECURITY_ALLOW_SCOPE_ERROR, + X_SECURITY_TYPE_DEFINITIONS, X_SECURITY_TYPE_DEFINITIONS_SCOPE_ERROR)); + + private static final ImmutableMap<ExtensionName, ImmutableSet<Class<? extends OpenApiSchema>>> + allowedParentsMap = + Maps.immutableEnumMap( + ImmutableMap.of( + X_SECURITY_TYPE, ImmutableSet.of(Schema.class, Parameter.class), + X_SECURITY_ALLOW, ImmutableSet.of(Operation.class), + X_SECURITY_TYPE_DEFINITIONS, ImmutableSet.of(OpenApi3.class))); + + /** + * Validates the scope of an supported {@link Extension} by contrasting it against its appropriate + * scope. + * + * <p>An invalid scoped Extension will return {@code INVALID_SCOPE} {@link + * ExtensionValidationMessage}. + */ + @Override + public ImmutableSet<ExtensionValidationMessage> validate(Extension extension) { + Optional<ExtensionName> extensionNameOptional = + valueOfExtensionName(extension.getExtensionName()); + ImmutableSet.Builder<ExtensionValidationMessage> errors = ImmutableSet.builder(); + + extensionNameOptional.ifPresentOrElse( + extensionNameEnum -> errors.addAll(validateExtensionScope(extension, extensionNameEnum)), + () -> errors.addAll(defaultErrors(extension))); + + logger.atInfo().log("Scope Errors(%s) : %s", prepareNamedPath(extension), errors); + + return errors.build(); + } + + /** + * Checks whether {@link Extension} exists under an allowed parent class. + * + * <p>A parent class is allowed if {@link Extension} has an entry in {@link + * BaseExtensionScopeValidator#allowedParentsMap} with it. + */ + private static ImmutableSet<ExtensionValidationMessage> validateExtensionScope( + Extension extension, ExtensionName extensionNameEnum) { + + Optional<Class<? extends OpenApiSchema>> extensionParent = fetchExtensionParent(extension); + + if (extensionParent.isPresent() + && allowedParentsMap.get(extensionNameEnum).contains(extensionParent.get())) { + return ImmutableSet.of(); + } + + return ImmutableSet.of(prepareInvalidScopeValidationMessage(extension, extensionNameEnum)); + } + + private static ExtensionValidationMessage prepareInvalidScopeValidationMessage( + Extension extension, ExtensionName extensionNameEnum) { + + ErrorMessage errorMessageEnum = errorMessages.get(extensionNameEnum); + return ExtensionValidationMessage.builder() + .setType(errorMessageEnum.getErrorType()) + .setMessage(errorMessageEnum.getErrorMessage()) + .setPath(prepareNamedPath(extension)) + .build(); + } +}
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ErrorMessage.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ErrorMessage.java new file mode 100644 index 0000000..40b6c53 --- /dev/null +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ErrorMessage.java
@@ -0,0 +1,30 @@ +package com.apigee.security.oas.extendedvalidator; + +/** Stores information of {@link Extension} validation errors for ease of use. */ +enum ErrorMessage { + UNSUPPORTED_EXTENSION("UNSUPPORTED_EXTENSION", "extension is not binded to a validator."), + X_SECURITY_TYPE_SCOPE_ERROR( + "INVALID_SCOPE", "extension should be under a schema or parameter object."), + X_SECURITY_ALLOW_SCOPE_ERROR( + "INVALID_SCOPE", + "extension should be under a paths operation object like get, post, patch, etc."), + X_SECURITY_TYPE_DEFINITIONS_SCOPE_ERROR( + "INVALID_SCOPE", "extension should be on the top-level scope."), + INVALID_SCHEMA("INVALID_SCHEMA", "extension has an invalid schema."); + + private final String errorType; + private final String errorMessage; + + ErrorMessage(String errorType, String errorMessage) { + this.errorType = errorType; + this.errorMessage = errorMessage; + } + + String getErrorType() { + return errorType; + } + + String getErrorMessage() { + return errorMessage; + } +}
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionModule.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionModule.java index 70c3f7a..bd083e2 100644 --- a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionModule.java +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionModule.java
@@ -19,6 +19,7 @@ .build(ExtensionFactory.class)); bind(ExtensionSchemaValidator.class).to(BaseExtensionSchemaValidator.class); + bind(ExtensionScopeValidator.class).to(BaseExtensionScopeValidator.class); } @Provides
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionScopeValidator.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionScopeValidator.java new file mode 100644 index 0000000..bb0e888 --- /dev/null +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionScopeValidator.java
@@ -0,0 +1,9 @@ +package com.apigee.security.oas.extendedvalidator; + +/** + * Validates that {@link Extension} exists in a valid scope. + * + * <p>A valid scope of {@link Extension} is a defined {@link Class}<? extends {@link + * org.openapi4j.parser.model.OpenApiSchema}> object that it is allowed to exist under. + */ +interface ExtensionScopeValidator extends ExtensionValidator {}
diff --git a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionValidators.java b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionValidators.java index d380ca7..1d85f31 100644 --- a/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionValidators.java +++ b/oas-core/src/main/java/com/apigee/security/oas/extendedvalidator/ExtensionValidators.java
@@ -1,5 +1,7 @@ package com.apigee.security.oas.extendedvalidator; +import static com.apigee.security.oas.extendedvalidator.ErrorMessage.UNSUPPORTED_EXTENSION; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.networknt.schema.ValidationMessage; @@ -72,11 +74,15 @@ return ImmutableSet.copyOf(updatedErrors); } - // TODO(b/162938113) : Create validation error type and message constants. /** Prepares {@link ExtensionValidationMessage} for extensions without validators. */ static ImmutableSet<ExtensionValidationMessage> defaultErrors(Extension extension) { - String errorType = "UNSUPPORTED_EXTENSION"; - String errorMessage = extension.getExtensionName() + " is not binded to the validator."; + String errorType = UNSUPPORTED_EXTENSION.getErrorType(); + String errorMessage = + new StringBuilder() + .append(extension.getExtensionName()) + .append(" ") + .append(UNSUPPORTED_EXTENSION.getErrorMessage()) + .toString(); String path = prepareNamedPath(extension); return ImmutableSet.of( ExtensionValidationMessage.builder() @@ -86,5 +92,16 @@ .build()); } + static Optional<Class<? extends OpenApiSchema>> fetchExtensionParent(Extension extension) { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> extensionPath = + extension.getExtensionPath(); + + if (extensionPath.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(extensionPath.get(extensionPath.size() - 1).getKey()); + } + private ExtensionValidators() {} }
diff --git a/oas-core/src/test/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidatorTest.java b/oas-core/src/test/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidatorTest.java new file mode 100644 index 0000000..aab22ec --- /dev/null +++ b/oas-core/src/test/java/com/apigee/security/oas/extendedvalidator/BaseExtensionScopeValidatorTest.java
@@ -0,0 +1,154 @@ +package com.apigee.security.oas.extendedvalidator; + +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_ALLOW; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_TYPE; +import static com.apigee.security.oas.extendedvalidator.ExtensionName.X_SECURITY_TYPE_DEFINITIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.inject.Injector; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; +import org.openapi4j.parser.model.OpenApiSchema; +import org.openapi4j.parser.model.v3.OpenApi3; +import org.openapi4j.parser.model.v3.Operation; +import org.openapi4j.parser.model.v3.Parameter; +import org.openapi4j.parser.model.v3.Path; + +@RunWith(JUnit4.class) +public class BaseExtensionScopeValidatorTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + @Mock private Extension extension; + + private ExtensionValidator validator; + + /** Setting up injector and scope validator instances. */ + @Before + public void setup() { + Injector injector = Guice.createInjector(new ExtendedValidatorMainModule()); + validator = injector.getInstance(ExtensionScopeValidator.class); + } + + private static ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> + createPathList(Class<? extends OpenApiSchema>[] pathClasses) { + ArrayList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> list = new ArrayList<>(); + for (Class<? extends OpenApiSchema> pathClass : pathClasses) { + list.add(new SimpleImmutableEntry<>(pathClass, Optional.empty())); + } + + return ImmutableList.copyOf(list); + } + + @Test + public void validate_unsupportedExtension_returnsUnsupportedExtensionErrorType() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + ImmutableList.of(); + when(extension.getExtensionName()).thenReturn("x-unsupported"); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)) + .hasSize(1) + .first() + .extracting(ExtensionValidationMessage::type) + .isEqualTo("UNSUPPORTED_EXTENSION"); + } + + @Test + public void validate_emptyPathSecurityTypeExtension_returnsInvalidScopeErrorType() { + when(extension.getExtensionName()).thenReturn(X_SECURITY_TYPE.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(ImmutableList.of()); + + assertThat(validator.validate(extension)) + .hasSize(1) + .first() + .extracting(ExtensionValidationMessage::type) + .isEqualTo("INVALID_SCOPE"); + } + + @Test + public void validate_validScopeSecurityTypeExtension_returnsEmptyErrorSet() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {Path.class, Operation.class, Parameter.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_TYPE.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)).isEmpty(); + } + + @Test + public void validate_invalidScopeSecurityTypeExtension_returnsInvalidScopeErrorType() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {Path.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_TYPE.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)) + .hasSize(1) + .first() + .extracting(ExtensionValidationMessage::type) + .isEqualTo("INVALID_SCOPE"); + } + + @Test + public void validate_validScopeSecurityAllowExtension_returnsEmptyErrorSet() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {Path.class, Operation.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_ALLOW.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)).isEmpty(); + } + + @Test + public void validate_invalidScopeSecurityAllowExtension_returnsInvalidScopeErrorTypes() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {Path.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_ALLOW.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)) + .hasSize(1) + .first() + .extracting(ExtensionValidationMessage::type) + .isEqualTo("INVALID_SCOPE"); + } + + @Test + public void validate_validScopeSecurityTypeDefinitionsExtension_returnsEmptyErrorSet() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {OpenApi3.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_TYPE_DEFINITIONS.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)).isEmpty(); + } + + @Test + public void validate_invalidScopeSecurityTypeDefinitionsExtension_returnsInvalidScopeErrorType() { + ImmutableList<Map.Entry<Class<? extends OpenApiSchema>, Optional<String>>> path = + createPathList(new Class[] {OpenApi3.class, Path.class}); + when(extension.getExtensionName()).thenReturn(X_SECURITY_TYPE_DEFINITIONS.getExtensionName()); + when(extension.getExtensionPath()).thenReturn(path); + + assertThat(validator.validate(extension)) + .hasSize(1) + .first() + .extracting(ExtensionValidationMessage::type) + .isEqualTo("INVALID_SCOPE"); + } +}