This is an automated email from the ASF dual-hosted git repository.

rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new 0069fd3  Allow OPTIONS response on web actions before checking for 
authentication requirement.
0069fd3 is described below

commit 0069fd36af8838ef26230d6f01a71da35cc2d26e
Author: Rodric Rabbah <[email protected]>
AuthorDate: Tue Apr 14 22:57:12 2020 -0400

    Allow OPTIONS response on web actions before checking for authentication 
requirement.
---
 .../openwhisk/core/controller/WebActions.scala     | 182 ++++++++++---------
 .../core/controller/test/WebActionsApiTests.scala  | 192 ++++++++++++++-------
 2 files changed, 223 insertions(+), 151 deletions(-)

diff --git 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/WebActions.scala
 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/WebActions.scala
index a62a5cb..17536a5 100644
--- 
a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/WebActions.scala
+++ 
b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/WebActions.scala
@@ -175,7 +175,7 @@ protected[core] object WhiskWebActionsApi extends 
Directives {
   }
 
   /**
-   * Supported extensions, their default projection and transcoder to complete 
a request.
+   * Supported extensions and transcoder to complete a request.
    *
    * @param extension  the supported media types for action response
    * @param transcoder the HTTP decoder and terminator for the extension
@@ -450,11 +450,6 @@ trait WhiskWebActionsApi
    * extension is one of supported media types. An example is ".json" for a 
JSON response or ".html" for
    * an text/html response.
    *
-   * Optionally, the result form the action may be projected based on a named 
property. As in
-   * /web/some-namespace/some-package/some-action/some-property. If the 
property
-   * does not exist in the result then a NotFound error is generated. A path 
of properties may
-   * be supplied to project nested properties.
-   *
    * Actions may be exposed to this web proxy by adding an annotation 
("export" -> true).
    */
   def routes(user: Option[Identity])(implicit transid: TransactionId): Route = 
{
@@ -498,27 +493,43 @@ trait WhiskWebActionsApi
           
validateSize(isWhithinRange(e.contentLengthOption.getOrElse(0)))(transid, 
jsonPrettyPrinter) {
             requestMethodParamsAndPath { context =>
               provide(fullyQualifiedActionName(actionName)) { fullActionName =>
-                onComplete(verifyWebAction(fullActionName, 
onBehalfOf.isDefined)) {
+                onComplete(verifyWebAction(fullActionName)) {
                   case Success((actionOwnerIdentity, action)) =>
-                    val requiredAuthOk =
-                      requiredWhiskAuthSuccessful(action.annotations, 
context.headers).getOrElse(true)
-                    if (!requiredAuthOk) {
-                      logging.debug(
-                        this,
-                        "web action with require-whisk-auth was invoked 
without a matching x-require-whisk-auth header value")
-                      terminate(Unauthorized)
-                    } else if (!action.annotations
-                                 
.getAs[Boolean](Annotations.WebCustomOptionsAnnotationName)
-                                 .getOrElse(false)) {
+                    val actionDelegatesCors =
+                      
!action.annotations.getAs[Boolean](Annotations.WebCustomOptionsAnnotationName).getOrElse(false)
+
+                    if (actionDelegatesCors) {
                       respondWithHeaders(defaultCorsResponse(context.headers)) 
{
                         if (context.method == OPTIONS) {
                           complete(OK, HttpEntity.Empty)
                         } else {
-                          extractEntityAndProcessRequest(actionOwnerIdentity, 
action, extension, onBehalfOf, context, e)
+                          extractEntityAndProcessRequest(
+                            confirmAuthenticated(action.annotations, 
context.headers, onBehalfOf).getOrElse(true),
+                            actionOwnerIdentity,
+                            action,
+                            extension,
+                            onBehalfOf,
+                            context,
+                            e)
                         }
                       }
                     } else {
-                      extractEntityAndProcessRequest(actionOwnerIdentity, 
action, extension, onBehalfOf, context, e)
+                      val allowedToProceed = if (context.method != OPTIONS) {
+                        confirmAuthenticated(action.annotations, 
context.headers, onBehalfOf).getOrElse(true)
+                      } else {
+                        // invoke the action for OPTIONS even if user is not 
authorized
+                        // so that action can respond to option request
+                        true
+                      }
+
+                      extractEntityAndProcessRequest(
+                        allowedToProceed,
+                        actionOwnerIdentity,
+                        action,
+                        extension,
+                        onBehalfOf,
+                        context,
+                        e)
                     }
 
                   case Failure(t: RejectRequest) =>
@@ -549,12 +560,11 @@ trait WhiskWebActionsApi
    *         not entitled (throttled), package/action not found, action not 
web enabled,
    *         or request overrides final parameters
    */
-  private def verifyWebAction(actionName: FullyQualifiedEntityName, 
authenticated: Boolean)(
-    implicit transid: TransactionId) = {
+  private def verifyWebAction(actionName: FullyQualifiedEntityName)(implicit 
transid: TransactionId) = {
 
     // lookup the identity for the action namespace
     identityLookup(actionName.path.root) flatMap { actionOwnerIdentity =>
-      confirmExportedAction(actionLookup(actionName), authenticated) flatMap { 
a =>
+      confirmExportedAction(actionLookup(actionName)) flatMap { a =>
         checkEntitlement(actionOwnerIdentity, a) map { _ =>
           (actionOwnerIdentity, a)
         }
@@ -562,7 +572,8 @@ trait WhiskWebActionsApi
     }
   }
 
-  private def extractEntityAndProcessRequest(actionOwnerIdentity: Identity,
+  private def extractEntityAndProcessRequest(authorizedToProceed: Boolean,
+                                             actionOwnerIdentity: Identity,
                                              action: WhiskActionMetaData,
                                              extension: MediaExtension,
                                              onBehalfOf: Option[Identity],
@@ -573,40 +584,46 @@ trait WhiskWebActionsApi
       processRequest(actionOwnerIdentity, action, extension, onBehalfOf, 
context.withBody(body), isRawHttpAction)
     }
 
-    
provide(action.annotations.getAs[Boolean](Annotations.RawHttpAnnotationName).getOrElse(false))
 { isRawHttpAction =>
-      httpEntity match {
-        case Empty =>
-          process(None, isRawHttpAction)
+    if (authorizedToProceed) {
+      
provide(action.annotations.getAs[Boolean](Annotations.RawHttpAnnotationName).getOrElse(false))
 {
+        isRawHttpAction =>
+          httpEntity match {
+            case Empty =>
+              process(None, isRawHttpAction)
+
+            case HttpEntity.Strict(ct, json) if 
WhiskWebActionsApi.isJsonFamily(ct.mediaType) && !isRawHttpAction =>
+              if (json.nonEmpty) {
+                entity(as[JsValue]) { body =>
+                  process(Some(body), isRawHttpAction)
+                }
+              } else {
+                process(None, isRawHttpAction)
+              }
 
-        case HttpEntity.Strict(ct, json) if 
WhiskWebActionsApi.isJsonFamily(ct.mediaType) && !isRawHttpAction =>
-          if (json.nonEmpty) {
-            entity(as[JsValue]) { body =>
-              process(Some(body), isRawHttpAction)
-            }
-          } else {
-            process(None, isRawHttpAction)
-          }
+            case 
HttpEntity.Strict(ContentType(MediaTypes.`application/x-www-form-urlencoded`, 
_), _)
+                if !isRawHttpAction =>
+              entity(as[FormData]) { form =>
+                val body = form.fields.toMap.toJson.asJsObject
+                process(Some(body), isRawHttpAction)
+              }
 
-        case 
HttpEntity.Strict(ContentType(MediaTypes.`application/x-www-form-urlencoded`, 
_), _) if !isRawHttpAction =>
-          entity(as[FormData]) { form =>
-            val body = form.fields.toMap.toJson.asJsObject
-            process(Some(body), isRawHttpAction)
-          }
+            case HttpEntity.Strict(contentType, data) =>
+              // for legacy, we are encoding application/json still
+              if (contentType.mediaType.binary || contentType.mediaType == 
`application/json`) {
+                Try(JsString(Base64.getEncoder.encodeToString(data.toArray))) 
match {
+                  case Success(bytes) => process(Some(bytes), isRawHttpAction)
+                  case Failure(t)     => terminate(BadRequest, 
Messages.unsupportedContentType(contentType.mediaType))
+                }
+              } else {
+                val str = JsString(data.utf8String)
+                process(Some(str), isRawHttpAction)
+              }
 
-        case HttpEntity.Strict(contentType, data) =>
-          // for legacy, we are encoding application/json still
-          if (contentType.mediaType.binary || contentType.mediaType == 
`application/json`) {
-            Try(JsString(Base64.getEncoder.encodeToString(data.toArray))) 
match {
-              case Success(bytes) => process(Some(bytes), isRawHttpAction)
-              case Failure(t)     => terminate(BadRequest, 
Messages.unsupportedContentType(contentType.mediaType))
-            }
-          } else {
-            val str = JsString(data.utf8String)
-            process(Some(str), isRawHttpAction)
+            case _ => terminate(BadRequest, Messages.unsupportedContentType)
           }
-
-        case _ => terminate(BadRequest, Messages.unsupportedContentType)
       }
+    } else {
+      terminate(Unauthorized)
     }
   }
 
@@ -648,9 +665,8 @@ trait WhiskWebActionsApi
             val resultPath = if (activation.response.isSuccess) {
               List.empty
             } else {
-              // the activation produced an error response: therefore ignore
-              // the requested projection and unwrap the error instead
-              // and attempt to handle it per the desired response type 
(extension)
+              // the activation produced an error response, so look for an 
error property
+              // in the response, unwrap it and use it to terminate the 
response
               List(ActivationResponse.ERROR_FIELD)
             }
 
@@ -717,24 +733,19 @@ trait WhiskWebActionsApi
 
   /**
    * Checks if an action is exported (i.e., carries the required annotation).
+   * This function does not check if web action requires authentication.
    */
-  private def confirmExportedAction(actionLookup: Future[WhiskActionMetaData], 
authenticated: Boolean)(
+  private def confirmExportedAction(actionLookup: Future[WhiskActionMetaData])(
     implicit transid: TransactionId): Future[WhiskActionMetaData] = {
     actionLookup flatMap { action =>
-      val requiresAuthenticatedUser =
-        
action.annotations.getAs[Boolean](Annotations.RequireWhiskAuthAnnotation).getOrElse(false)
       val isExported = 
action.annotations.getAs[Boolean](Annotations.WebActionAnnotationName).getOrElse(false)
 
-      if ((isExported && requiresAuthenticatedUser && authenticated) ||
-          (isExported && !requiresAuthenticatedUser)) {
+      if (isExported) {
         logging.debug(this, s"${action.fullyQualifiedName(true)} is exported")
         Future.successful(action)
-      } else if (!isExported) {
+      } else {
         logging.debug(this, s"${action.fullyQualifiedName(true)} not exported")
         Future.failed(RejectRequest(NotFound))
-      } else {
-        logging.debug(this, s"${action.fullyQualifiedName(true)} requires 
authentication")
-        Future.failed(RejectRequest(Unauthorized))
       }
     }
   }
@@ -751,30 +762,33 @@ trait WhiskWebActionsApi
   }
 
   /**
-   * Checks if "require-whisk-auth" authentication is needed, and if so, 
authenticate the request.
-   * NOTE: Only number or string JSON "require-whisk-auth" annotation values 
are supported.
+   * Checks if an action requires authenticate and is authenticated (i.e., 
carries the required annotation).
+   * This function assumes the action is a web action.
    *
-   * @param annotations - web action annotations
-   * @param reqHeaders  - web action invocation request headers
-   * @return Option[Boolean]
-   *         None if annotations does not include require-whisk-auth (i.e., 
auth test not needed)
-   *         Some(true) if annotations includes require-whisk-auth and its 
value matches the request header `X-Require-Whisk-Auth` value
-   *         Some(false) if annotations includes require-whisk-auth and the 
request does not include the header `X-Require-Whisk-Auth`
-   *         Some(false) if annotations includes require-whisk-auth and its 
value does not match the request header `X-Require-Whisk-Auth` value
+   * @param annotations the web action annotations
+   * @param reqHeaders the web action invocation request headers
+   * @param authenticatedUser true if this request is from an authenticated 
whisk user
+   * @return None if web annotation does not specify an authentication scheme
+   *         Some(true) if web annotation includes require-whisk-auth and 
value matches the request header `X-Require-Whisk-Auth` value
+   *         Some(true) if web annotation requires an authenticated whisk user 
and that user has already authenticated
+   *         Some(false) if web annotation includes require-whisk-auth and the 
request does not include the header `X-Require-Whisk-Auth`
+   *         Some(false) if web annotation includes require-whisk-auth and its 
value does not match the request header `X-Require-Whisk-Auth` value
    */
-  private def requiredWhiskAuthSuccessful(annotations: Parameters, reqHeaders: 
Seq[HttpHeader]): Option[Boolean] = {
+  private def confirmAuthenticated(annotations: Parameters,
+                                   reqHeaders: Seq[HttpHeader],
+                                   authenticatedUser: Option[Identity]): 
Option[Boolean] = {
+    def checkAuthHeader(expected: String): Boolean = {
+      reqHeaders.find(_.is(WhiskAction.requireWhiskAuthHeader)).map(_.value == 
expected).getOrElse(false)
+    }
+
     annotations
       .get(Annotations.RequireWhiskAuthAnnotation)
-      .flatMap {
-        case JsString(authStr) => Some(authStr)
-        case JsNumber(authNum) => Some(authNum.toString)
-        case _                 => None
-      }
-      .map { reqWhiskAuthAnnotationStr =>
-        reqHeaders
-          .find(_.is(WhiskAction.requireWhiskAuthHeader))
-          .map(_.value == reqWhiskAuthAnnotationStr)
-          .getOrElse(false) // false => when no x-require-whisk-auth header is 
present
+      .map {
+        case JsString(auth)           => checkAuthHeader(auth) // allowed if 
auth matches header
+        case JsNumber(auth)           => checkAuthHeader(auth.toString) // 
allowed if auth matches header
+        case JsTrue | JsBoolean(true) => authenticatedUser.isDefined // 
allowed if user already authenticated
+        case _                        => false // not allowed, something is 
not right
       }
   }
+
 }
diff --git 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
index e91b5a1..dd7ce68 100644
--- 
a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
+++ 
b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala
@@ -135,7 +135,6 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
   var failThrottleForSubject: Option[Subject] = None // toggle to cause 
throttle to fail for subject
   var failCheckEntitlement = false // toggle to cause entitlement to fail
   var actionResult: Option[JsObject] = None
-  var requireAuthenticationAsBoolean = true // toggle value set in 
require-whisk-auth annotation (true or requireAuthenticationKey)
   var testParametersInInvokeAction = true // toggle to test parameter in 
invokeAction
   var requireAuthenticationKey = "example-web-action-api-key"
   var invocationCount = 0
@@ -165,7 +164,6 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
     failThrottleForSubject = None
     failCheckEntitlement = false
     actionResult = None
-    requireAuthenticationAsBoolean = true
     testParametersInInvokeAction = true
     assert(invocationsAllowed == invocationCount, "allowed invoke count did 
not match actual")
     cleanup()
@@ -393,53 +391,97 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
         }
     }
 
-    it should s"reject requests when authentication is required but none given 
(auth? ${creds.isDefined})" in {
+    it should s"reject requests when whisk authentication is required but none 
given (auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
+      val entityName = MakeName.next("export")
+      val action =
+        stubAction(
+          proxyNamespace,
+          entityName,
+          customOptions = false,
+          requireAuthentication = true,
+          requireAuthenticationAsBoolean = true)
+      val path = action.fullyQualifiedName(false)
+      put(entityStore, action)
+
       allowedMethods.foreach { m =>
-        Seq(true, false).foreach { useReqWhiskAuthBool =>
-          requireAuthenticationAsBoolean = useReqWhiskAuthBool
+        m(s"$testRoutePath/${path}.json") ~> Route.seal(routes(creds)) ~> 
check {
+          if (m === Options) {
+            status should be(OK) // options response is always present 
regardless of auth
+            header("Access-Control-Allow-Origin").get.toString shouldBe 
"Access-Control-Allow-Origin: *"
+            header("Access-Control-Allow-Methods").get.toString shouldBe 
"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH"
+            header("Access-Control-Request-Headers") shouldBe empty
+          } else if (creds.isEmpty) {
+            status should be(Unauthorized) // if user is not authenticated, 
reject all requests
+          } else {
+            invocationsAllowed += 1
+            status should be(OK)
+            val response = responseAs[JsObject]
+            response shouldBe JsObject(
+              "pkg" -> s"$systemId/proxy".toJson,
+              "action" -> entityName.asString.toJson,
+              "content" -> metaPayload(m.method.name.toLowerCase, 
JsObject.empty, creds, pkgName = "proxy"))
+            response
+              .fields("content")
+              .asJsObject
+              .fields(webApiDirectives.namespace) shouldBe 
creds.get.namespace.name.toJson
+          }
         }
+      }
+    }
 
-        val entityName = MakeName.next("export")
-        val action = stubAction(
+    it should s"reject requests when x-authentication is required but none 
given (auth? ${creds.isDefined})" in {
+      implicit val tid = transid()
+
+      val entityName = MakeName.next("export")
+      val action =
+        stubAction(
           proxyNamespace,
           entityName,
+          customOptions = false,
           requireAuthentication = true,
-          requireAuthenticationAsBoolean = requireAuthenticationAsBoolean)
-        val path = action.fullyQualifiedName(false)
+          requireAuthenticationAsBoolean = false)
+      val path = action.fullyQualifiedName(false)
+      put(entityStore, action)
 
-        put(entityStore, action)
+      allowedMethods.foreach { m =>
+        // web action require-whisk-auth is set, but the header 
X-Require-Whisk-Auth value does not match
+        m(s"$testRoutePath/${path}.json") ~> addHeader(
+          WhiskAction.requireWhiskAuthHeader,
+          requireAuthenticationKey + "-bad") ~> Route
+          .seal(routes(creds)) ~> check {
+          if (m == Options) {
+            status should be(OK) // options should always respond
+            header("Access-Control-Allow-Origin").get.toString shouldBe 
"Access-Control-Allow-Origin: *"
+            header("Access-Control-Allow-Methods").get.toString shouldBe 
"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH"
+            header("Access-Control-Request-Headers") shouldBe empty
+          } else {
+            status should be(Unauthorized)
+          }
+        }
 
-        if (requireAuthenticationAsBoolean) {
-          if (creds.isDefined) {
-            val user = creds.get
-            invocationsAllowed += 1
-            m(s"$testRoutePath/${path}.json") ~> Route
-              .seal(routes(creds)) ~> check {
-              status should be(OK)
-              val response = responseAs[JsObject]
-              response shouldBe JsObject(
-                "pkg" -> s"$systemId/proxy".toJson,
-                "action" -> entityName.asString.toJson,
-                "content" -> metaPayload(m.method.name.toLowerCase, 
JsObject.empty, creds, pkgName = "proxy"))
-              response
-                .fields("content")
-                .asJsObject
-                .fields(webApiDirectives.namespace) shouldBe 
user.namespace.name.toJson
-            }
+        // web action require-whisk-auth is set, but the header 
X-Require-Whisk-Auth value is not set
+        m(s"$testRoutePath/${path}.json") ~> Route.seal(routes(creds)) ~> 
check {
+          if (m == Options) {
+            status should be(OK) // options should always respond
+            header("Access-Control-Allow-Origin").get.toString shouldBe 
"Access-Control-Allow-Origin: *"
+            header("Access-Control-Allow-Methods").get.toString shouldBe 
"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH"
+            header("Access-Control-Request-Headers") shouldBe empty
           } else {
-            m(s"$testRoutePath/${path}.json") ~> Route.seal(routes(creds)) ~> 
check {
-              status should be(Unauthorized)
-            }
+            status should be(Unauthorized)
           }
-        } else if (creds.isDefined) {
-          val user = creds.get
-          invocationsAllowed += 1
+        }
 
-          // web action require-whisk-auth is set and the header 
X-Require-Whisk-Auth value does not matches
-          m(s"$testRoutePath/${path}.json") ~> 
addHeader("X-Require-Whisk-Auth", requireAuthenticationKey) ~> Route
-            .seal(routes(creds)) ~> check {
+        m(s"$testRoutePath/${path}.json") ~> 
addHeader(WhiskAction.requireWhiskAuthHeader, requireAuthenticationKey) ~> Route
+          .seal(routes(creds)) ~> check {
+          if (m == Options) {
+            status should be(OK) // options should always respond
+            header("Access-Control-Allow-Origin").get.toString shouldBe 
"Access-Control-Allow-Origin: *"
+            header("Access-Control-Allow-Methods").get.toString shouldBe 
"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH"
+            header("Access-Control-Request-Headers") shouldBe empty
+          } else {
+            invocationsAllowed += 1
             status should be(OK)
             val response = responseAs[JsObject]
             response shouldBe JsObject(
@@ -450,22 +492,13 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
                 JsObject.empty,
                 creds,
                 pkgName = "proxy",
-                headers = List(RawHeader("X-Require-Whisk-Auth", 
requireAuthenticationKey))))
-            response
-              .fields("content")
-              .asJsObject
-              .fields(webApiDirectives.namespace) shouldBe 
user.namespace.name.toJson
-          }
-
-          // web action require-whisk-auth is set, but the header 
X-Require-Whisk-Auth value does not match
-          m(s"$testRoutePath/${path}.json") ~> 
addHeader("X-Require-Whisk-Auth", requireAuthenticationKey + "-bad") ~> Route
-            .seal(routes(creds)) ~> check {
-            status should be(Unauthorized)
-          }
-        } else {
-          // web action require-whisk-auth is set, but the header 
X-Require-Whisk-Auth value is not set
-          m(s"$testRoutePath/${path}.json") ~> Route.seal(routes(creds)) ~> 
check {
-            status should be(Unauthorized)
+                headers = List(RawHeader(WhiskAction.requireWhiskAuthHeader, 
requireAuthenticationKey))))
+            if (creds.isDefined) {
+              response
+                .fields("content")
+                .asJsObject
+                .fields(webApiDirectives.namespace) shouldBe 
creds.get.namespace.name.toJson
+            }
           }
         }
       }
@@ -824,7 +857,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       }
     }
 
-    it should s"not project a field from the result object (auth? 
${creds.isDefined})" in {
+    it should s"pass the unmatched segment to the action (auth? 
${creds.isDefined})" in {
       implicit val tid = transid()
 
       Seq(s"$systemId/proxy/export_c.json/content").foreach { path =>
@@ -845,10 +878,10 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       }
     }
 
-    it should s"reject when projecting a field from the result object that 
does not exist (auth? ${creds.isDefined})" in {
+    it should s"respond with error when expected text property does not exist 
(auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
-      Seq(s"$systemId/proxy/export_c.text/foobar", 
s"$systemId/proxy/export_c.text/content/z/x").foreach { path =>
+      Seq(s"$systemId/proxy/export_c.text").foreach { path =>
         allowedMethods.foreach { m =>
           invocationsAllowed += 1
 
@@ -862,11 +895,10 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       }
     }
 
-    it should s"not project an http response (auth? ${creds.isDefined})" in {
+    it should s"use action status code and headers to terminate an http 
response (auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
-      // http extension does not project
-      Seq(s"$systemId/proxy/export_c.http/content/response").foreach { path =>
+      Seq(s"$systemId/proxy/export_c.http").foreach { path =>
         allowedMethods.foreach { m =>
           actionResult = Some(
             JsObject(
@@ -882,7 +914,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       }
     }
 
-    it should s"use default projection for extension (auth? 
${creds.isDefined})" in {
+    it should s"use default field projection for extension (auth? 
${creds.isDefined})" in {
       implicit val tid = transid()
 
       Seq(s"$systemId/proxy/export_c.http").foreach { path =>
@@ -1378,10 +1410,10 @@ trait WebActionsApiBaseTests extends 
ControllerTestCommon with BeforeAndAfterEac
       }
     }
 
-    it should s"handle an activation that results in application error and 
response matches extension (auth? ${creds.isDefined})" in {
+    it should s"handle an activation that results in application error (auth? 
${creds.isDefined})" in {
       implicit val tid = transid()
 
-      Seq(s"$systemId/proxy/export_c.http", 
s"$systemId/proxy/export_c.http/ignoreme").foreach { path =>
+      Seq(s"$systemId/proxy/export_c.http").foreach { path =>
         allowedMethods.foreach { m =>
           invocationsAllowed += 1
           actionResult = Some(
@@ -1398,10 +1430,10 @@ trait WebActionsApiBaseTests extends 
ControllerTestCommon with BeforeAndAfterEac
       }
     }
 
-    it should s"handle an activation that results in application error but 
where response does not match extension (auth? ${creds.isDefined})" in {
+    it should s"handle an activation that results in application error that 
does not match .json extension (auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
-      Seq(s"$systemId/proxy/export_c.json", 
s"$systemId/proxy/export_c.json/ignoreme").foreach { path =>
+      Seq(s"$systemId/proxy/export_c.json").foreach { path =>
         allowedMethods.foreach { m =>
           invocationsAllowed += 1
           actionResult = Some(JsObject("application_error" -> "bad response 
type".toJson))
@@ -1417,7 +1449,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
     it should s"handle an activation that results in developer or system error 
(auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
-      Seq(s"$systemId/proxy/export_c.json", 
s"$systemId/proxy/export_c.json/ignoreme", s"$systemId/proxy/export_c.text")
+      Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.text")
         .foreach { path =>
           Seq("developer_error", "whisk_error").foreach { e =>
             allowedMethods.foreach { m =>
@@ -1627,11 +1659,11 @@ trait WebActionsApiBaseTests extends 
ControllerTestCommon with BeforeAndAfterEac
       }
     }
 
-    it should s"invoke action with options verb with custom options (auth? 
${creds.isDefined})" in {
+    it should s"respond with custom options (auth? ${creds.isDefined})" in {
       implicit val tid = transid()
 
       Seq(s"$systemId/proxy/export_c.http").foreach { path =>
-        invocationsAllowed += 1
+        invocationsAllowed += 1 // custom options means action is invoked
         actionResult =
           Some(JsObject("headers" -> JsObject("Access-Control-Allow-Methods" 
-> "OPTIONS, GET, PATCH".toJson)))
 
@@ -1645,6 +1677,32 @@ trait WebActionsApiBaseTests extends 
ControllerTestCommon with BeforeAndAfterEac
       }
     }
 
+    it should s"respond with custom options even when authentication is 
required but missing (auth? ${creds.isDefined})" in {
+      implicit val tid = transid()
+
+      val entityName = MakeName.next("export")
+      val action =
+        stubAction(
+          proxyNamespace,
+          entityName,
+          customOptions = true,
+          requireAuthentication = true,
+          requireAuthenticationAsBoolean = true)
+      val path = action.fullyQualifiedName(false)
+      put(entityStore, action)
+
+      invocationsAllowed += 1 // custom options means action is invoked
+      actionResult =
+        Some(JsObject("headers" -> JsObject("Access-Control-Allow-Methods" -> 
"OPTIONS, GET, PATCH".toJson)))
+
+      // the added headers should be ignored
+      Options(s"$testRoutePath/$path") ~> Route.seal(routes(creds)) ~> check {
+        header("Access-Control-Allow-Origin") shouldBe empty
+        header("Access-Control-Allow-Methods").get.toString shouldBe 
"Access-Control-Allow-Methods: OPTIONS, GET, PATCH"
+        header("Access-Control-Request-Headers") shouldBe empty
+      }
+    }
+
     it should s"support multiple values for headers (auth? 
${creds.isDefined})" in {
       implicit val tid = transid()
 
@@ -1660,7 +1718,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon 
with BeforeAndAfterEac
       }
     }
 
-    it should s"invoke action with options verb without custom options (auth? 
${creds.isDefined})" in {
+    it should s"invoke action and respond with default options headers (auth? 
${creds.isDefined})" in {
       implicit val tid = transid()
 
       put(entityStore, stubAction(proxyNamespace, 
EntityName("export_without_custom_options"), false))

Reply via email to