Merge "b/163430475 Refactor traverse methods to return extensions"
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");
+  }
+}