Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
}

/** Remove parameter by name. */
protected[core] def -(p: String) = {
protected[core] def -(p: String): Parameters = {
// wrap with try since parameter name may throw an exception for illegal p
Try(new Parameters(params - new ParameterName(p))) getOrElse this
}
Expand Down Expand Up @@ -103,15 +103,20 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
.fold[Try[JsValue]](Failure(new IllegalStateException(s"key '$p' does not exist")))(Success.apply)
.flatMap(js => Try(js.convertTo[T]))

/** Retrieves parameter by name if it exist. Returns true if parameter exists and has truthy value. */
protected[core] def isTruthy(p: String): Boolean = {
/**
* Retrieves parameter by name if it exist.
* @param p the parameter to check for a truthy value
* @param valueForNonExistent the value to return for a missing parameter (default false)
* @return true if parameter exists and has truthy value, otherwise returns the specified value for non-existent keys
*/
protected[core] def isTruthy(p: String, valueForNonExistent: Boolean = false): Boolean = {
get(p) map {
case JsBoolean(b) => b
case JsNumber(n) => n != 0
case JsString(s) => s.nonEmpty
case JsNull => false
case _ => true
} getOrElse false
} getOrElse valueForNonExistent
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ case class WhiskAction(namespace: EntityPath,
* Merges parameters (usually from package) with existing action parameters.
* Existing parameters supersede those in p.
*/
def inherit(p: Parameters) = copy(parameters = p ++ parameters).revision[WhiskAction](rev)
def inherit(p: Parameters): WhiskAction = copy(parameters = p ++ parameters).revision[WhiskAction](rev)

/**
* Resolves sequence components if they contain default namespace.
Expand All @@ -166,7 +166,7 @@ case class WhiskAction(namespace: EntityPath,
}
}

def toExecutableWhiskAction = exec match {
def toExecutableWhiskAction: Option[ExecutableWhiskAction] = exec match {
case codeExec: CodeExec[_] =>
Some(
ExecutableWhiskAction(namespace, name, codeExec, parameters, limits, version, publish, annotations)
Expand All @@ -178,7 +178,7 @@ case class WhiskAction(namespace: EntityPath,
* This the action summary as computed by the database view.
* Strictly used in view testing to enforce alignment.
*/
override def summaryAsJson = {
override def summaryAsJson: JsObject = {
val binary = exec match {
case c: CodeExec[_] => c.binary
case _ => false
Expand Down Expand Up @@ -309,6 +309,12 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[

val execFieldName = "exec"
val finalParamsAnnotationName = "final"
val webActionAnnotationName = "web-export"
val webCustomOptionsAnnotationName = "web-custom-options"
val rawHttpAnnotationName = "raw-http"
val requireWhiskAuthAnnotation = "require-whisk-auth"
val requireWhiskAuthHeader = "x-require-whisk-auth"
val provideApiKeyAnnotationName = "provide-api-key"

override val collectionName = "actions"

Expand All @@ -325,9 +331,6 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[

override val cacheEnabled = true

val requireWhiskAuthAnnotation = "require-whisk-auth"
val requireWhiskAuthHeader = "x-require-whisk-auth"

// overriden to store attached code
override def put[A >: WhiskAction](db: ArtifactStore[A], doc: WhiskAction, old: Option[WhiskAction])(
implicit transid: TransactionId,
Expand Down Expand Up @@ -512,9 +515,6 @@ object WhiskActionMetaData
with WhiskEntityQueries[WhiskActionMetaData]
with DefaultJsonProtocol {

val execFieldName = "exec"
val finalParamsAnnotationName = "final"

override val collectionName = "actions"

override implicit val serdes = jsonFormat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,38 @@ object WhiskActionsApi {
* This is the default timeout on a POST request.
*/
protected[core] val maxWaitForBlockingActivation = 60 seconds

/**
* Amends annotations on an action create/update with system defined values.
* This method currently adds the following annotations:
* 1. [[WhiskAction.provideApiKeyAnnotationName]] with the value false iff the annotation is not already defined in the action annotations
* 2. An [[execAnnotation]] consistent with the action kind; this annotation is always added and overrides a pre-existing value
*/
protected[core] def amendAnnotations(annotations: Parameters, exec: Exec, create: Boolean = true): Parameters = {
val newAnnotations = if (create) {
// these annotations are only added on newly created actions
// since they can break existing actions created before the
// annotation was created
annotations
.get(WhiskAction.provideApiKeyAnnotationName)
.map(_ => annotations)
.getOrElse {
annotations ++ Parameters(WhiskAction.provideApiKeyAnnotationName, JsBoolean(false))
}
} else annotations
newAnnotations ++ execAnnotation(exec)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


/**
* Constructs an "exec" annotation. This is redundant with the exec kind
* information available in WhiskAction but necessary for some clients which
* fetch action lists but cannot determine action kinds without fetching them.
* An alternative is to include the exec in the action list "view" but this
* will require an API change. So using an annotation instead.
*/
private def execAnnotation(exec: Exec): Parameters = {
Parameters(WhiskAction.execFieldName, exec.kind)
}
}

/** A trait implementing the actions API. */
Expand Down Expand Up @@ -416,7 +448,7 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
limits,
content.version getOrElse SemVer(),
content.publish getOrElse false,
(content.annotations getOrElse Parameters()) ++ execAnnotation(exec))
WhiskActionsApi.amendAnnotations(content.annotations getOrElse Parameters(), exec))
}

/** For a sequence action, gather referenced entities and authorize access. */
Expand Down Expand Up @@ -531,7 +563,7 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
limits,
content.version getOrElse action.version.upPatch,
content.publish getOrElse action.publish,
(content.annotations getOrElse action.annotations) ++ execAnnotation(exec))
WhiskActionsApi.amendAnnotations(content.annotations getOrElse action.annotations, exec, create = false))
.revision[WhiskAction](action.docinfo.rev)
}

Expand Down Expand Up @@ -685,17 +717,6 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
}
}

/**
* Constructs an "exec" annotation. This is redundant with the exec kind
* information available in WhiskAction but necessary for some clients which
* fetch action lists but cannot determine action kinds without fetching them.
* An alternative is to include the exec in the action list "view" but this
* will require an API change. So using an annotation instead.
*/
private def execAnnotation(exec: Exec): Parameters = {
Parameters(WhiskAction.execFieldName, exec.kind)
}

/** Max atomic action count allowed for sequences. */
private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ trait WhiskWebActionsApi
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]("web-custom-options").getOrElse(false)) {
} else if (!action.annotations
.getAs[Boolean](WhiskAction.webCustomOptionsAnnotationName)
.getOrElse(false)) {
respondWithHeaders(defaultCorsResponse(context.headers)) {
if (context.method == OPTIONS) {
complete(OK, HttpEntity.Empty)
Expand Down Expand Up @@ -554,7 +556,7 @@ trait WhiskWebActionsApi
processRequest(actionOwnerIdentity, action, extension, onBehalfOf, context.withBody(body), isRawHttpAction)
}

provide(action.annotations.getAs[Boolean]("raw-http").getOrElse(false)) { isRawHttpAction =>
provide(action.annotations.getAs[Boolean](WhiskAction.rawHttpAnnotationName).getOrElse(false)) { isRawHttpAction =>
httpEntity match {
case Empty =>
process(None, isRawHttpAction)
Expand Down Expand Up @@ -705,7 +707,7 @@ trait WhiskWebActionsApi
actionLookup flatMap { action =>
val requiresAuthenticatedUser =
action.annotations.getAs[Boolean](WhiskAction.requireWhiskAuthAnnotation).getOrElse(false)
val isExported = action.annotations.getAs[Boolean]("web-export").getOrElse(false)
val isExported = action.annotations.getAs[Boolean](WhiskAction.webActionAnnotationName).getOrElse(false)

if ((isExported && requiresAuthenticatedUser && authenticated) ||
(isExported && !requiresAuthenticatedUser)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,13 @@ class ContainerProxy(
}
val parameters = job.msg.content getOrElse JsObject.empty

val authEnvironment = job.msg.user.authkey.toEnvironment
// if the action requests the api key to be injected into the action context, add it here;
// treat a missing annotation as requesting the api key for backward compatibility
val authEnvironment = {
if (job.action.annotations.isTruthy(WhiskAction.provideApiKeyAnnotationName, valueForNonExistent = true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@mhenke1 mhenke1 Feb 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I second to stay backwards compatible.

job.msg.user.authkey.toEnvironment
} else JsObject.empty
Copy link
Contributor

@mhenke1 mhenke1 Feb 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else job.msg.user.authkey.toEnvironmentNotSensitive

I do suggest to move the decision what to hide as sensitive information to each GenericAuthKey implementation by adding a new method authkey.toEnvironmentNotSensitive.
For BasicAuthenticationAuthKey this method should return JsObject.empty
Other GenericAuthKey implementations could decide which of the information stored should be made available on request only.

If we stay backwards compatible , I would be willing to implement this change implement in a following PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you send a comment to the dev list with the proposal. It is reasonable to me. Do you have an example you can attach?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address it once this PR is merged. No need to delay.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

}

val environment = JsObject(
"namespace" -> job.msg.user.namespace.name.toJson,
Expand Down
12 changes: 6 additions & 6 deletions docs/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,9 @@ or set an internal alarm when the action is about to use up its allotted time bu
The properties are accessible via the system environment for all supported runtimes:
Node.js, Python, Swift, Java and Docker actions when using the OpenWhisk Docker skeleton.

* `__OW_API_HOST` the API host for the OpenWhisk deployment running this action
* `__OW_API_KEY` the API key for the subject invoking the action, this key may be a restricted API key
* `__OW_NAMESPACE` the namespace for the _activation_ (this may not be the same as the namespace for the action)
* `__OW_ACTION_NAME` the fully qualified name of the running action
* `__OW_ACTIVATION_ID` the activation id for this running action instance
* `__OW_DEADLINE` the approximate time when this action will have consumed its entire duration quota (measured in epoch milliseconds)
* `__OW_API_HOST` the API host for the OpenWhisk deployment running this action.
* `__OW_API_KEY` the API key for the subject invoking the action, this key may be a restricted API key. This property is absent unless explicitly [requested](./annotations.md#annotations-for-all-actions).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to update the docs explaining the reverse compatibility feature you added.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in last commit.

* `__OW_NAMESPACE` the namespace for the _activation_ (this may not be the same as the namespace for the action).
* `__OW_ACTION_NAME` the fully qualified name of the running action.
* `__OW_ACTIVATION_ID` the activation id for this running action instance.
* `__OW_DEADLINE` the approximate time when this action will have consumed its entire duration quota (measured in epoch milliseconds).
9 changes: 9 additions & 0 deletions docs/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ The annotations we have used for describing parameters include:

The annotations are _not_ checked. So while it is conceivable to use the annotations to infer if a composition of two actions into a sequence is legal, for example, the system does not yet do that.

# Annotations for all actions

The following annotations on an action are available.

* `provide-api-key`: This annotation may be attached to actions which require an API key, for example to make REST API calls to the OpenWhisk host.
The absence of this annotation, or its presence with a value that is not _falsy_ (i.e., a value that is different from zero, null, false, and the empty string)
will cause an API key to be present in the [action execution context](./actions.md#accessing-action-metadata-within-the-action-body). This annotation is added
to newly created actions, if not already specified, with a default false value.

# Annotations specific to web actions

Web actions are enabled with explicit annotations which decorate individual actions. The annotations only apply to the [web actions](webactions.md) API,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,10 @@ class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskAct
it should "invoke an action using npm openwhisk" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val name = "hello npm openwhisk"
assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloOpenwhiskPackage.js")))
action.create(
name,
Some(TestUtils.getTestActionFilename("helloOpenwhiskPackage.js")),
annotations = Map(WhiskAction.provideApiKeyAnnotationName -> JsBoolean(true)))
}

val run = wsk.action.invoke(name, Map("ignore_certs" -> true.toJson, "name" -> name.toJson))
Expand All @@ -313,25 +316,51 @@ class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskAct
wsk.action.delete(name, expectedExitCode = NotFound.intValue)
}

it should "invoke an action receiving context properties" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
val namespace = wsk.namespace.whois()
val name = "context"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloContext.js")))
}
it should "invoke an action receiving context properties excluding api key" in withAssetCleaner(wskprops) {
(wp, assetHelper) =>
val namespace = wsk.namespace.whois()
val name = "context"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(name, Some(TestUtils.getTestActionFilename("helloContext.js")))
}

val start = Instant.now(Clock.systemUTC()).toEpochMilli
val run = wsk.action.invoke(name)
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
val fields = activation.response.result.get.convertTo[Map[String, String]]
fields("api_host") shouldBe WhiskProperties.getApiHostForAction
fields("api_key") shouldBe wskprops.authKey
fields("namespace") shouldBe namespace
fields("action_name") shouldBe s"/$namespace/$name"
fields("activation_id") shouldBe activation.activationId
fields("deadline").toLong should be >= start
}
val start = Instant.now(Clock.systemUTC()).toEpochMilli
val run = wsk.action.invoke(name)
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
val fields = activation.response.result.get.convertTo[Map[String, String]]
fields("api_host") shouldBe WhiskProperties.getApiHostForAction
fields.get("api_key") shouldBe empty
fields("namespace") shouldBe namespace
fields("action_name") shouldBe s"/$namespace/$name"
fields("activation_id") shouldBe activation.activationId
fields("deadline").toLong should be >= start
}
}

it should "invoke an action receiving context properties including api key" in withAssetCleaner(wskprops) {
(wp, assetHelper) =>
val namespace = wsk.namespace.whois()
val name = "context"
assetHelper.withCleaner(wsk.action, name) { (action, _) =>
action.create(
name,
Some(TestUtils.getTestActionFilename("helloContext.js")),
annotations = Map(WhiskAction.provideApiKeyAnnotationName -> JsBoolean(true)))
}

val start = Instant.now(Clock.systemUTC()).toEpochMilli
val run = wsk.action.invoke(name)
withActivation(wsk.activation, run) { activation =>
activation.response.status shouldBe "success"
val fields = activation.response.result.get.convertTo[Map[String, String]]
fields("api_host") shouldBe WhiskProperties.getApiHostForAction
fields("api_key") shouldBe wskprops.authKey
fields("namespace") shouldBe namespace
fields("action_name") shouldBe s"/$namespace/$name"
fields("activation_id") shouldBe activation.activationId
fields("deadline").toLong should be >= start
}
}

it should "invoke an action successfully with options --blocking and --result" in withAssetCleaner(wskprops) {
Expand Down Expand Up @@ -404,6 +433,7 @@ class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskAct
val action = wsk.action.get(name)
action.getFieldJsValue("annotations").convertTo[Set[JsObject]] shouldBe Set(
JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")),
JsObject("key" -> WhiskAction.provideApiKeyAnnotationName.toJson, "value" -> JsBoolean(false)),
JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(webEnabled || rawEnabled)),
JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(rawEnabled)),
JsObject("key" -> JsString("final"), "value" -> JsBoolean(webEnabled || rawEnabled)))
Expand All @@ -424,6 +454,7 @@ class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskAct
JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)),
JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)),
JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)),
JsObject("key" -> WhiskAction.provideApiKeyAnnotationName.toJson, "value" -> JsBoolean(false)),
JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")))
}

Expand Down
Loading