You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ra...@apache.org on 2020/04/18 20:26:48 UTC
[openwhisk] branch master updated: Allow OPTIONS response on web
actions before checking for authentication requirement.
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 <ro...@gmail.com>
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))